/*
 *    Geotoolkit.org - An Open Source Java GIS Toolkit
 *    http://www.geotoolkit.org
 *
 *    (C) 2007-2012, Open Source Geospatial Foundation (OSGeo)
 *    (C) 2009-2012, Geomatys
 *
 *    This library is free software; you can redistribute it and/or
 *    modify it under the terms of the GNU Lesser General Public
 *    License as published by the Free Software Foundation;
 *    version 2.1 of the License.
 *
 *    This library is distributed in the hope that it will be useful,
 *    but WITHOUT ANY WARRANTY; without even the implied warranty of
 *    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the GNU
 *    Lesser General Public License for more details.
 */
package org.geotoolkit.util.logging;

import java.text.AttributedCharacterIterator;
import java.text.FieldPosition;
import java.text.Format;
import java.text.ParseException;
import java.text.ParsePosition;
import java.util.Locale;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.logging.LogRecord;

import org.geotoolkit.resources.Errors;
import org.apache.sis.util.Classes;
import org.apache.sis.util.CharSequences;


/**
 * Wraps a {@link Format} object in order to either parse fully a string, or log a warning.
 * This class provides a {@link #parse(String)} method which performs the following tasks:
 * <p>
 * <ul>
 *   <li>Checks if the string was fully parsed and log a warning if it was not. This is
 *       different than the default {@link #parseObject(String)} behavior which check only
 *       if the <em>beginning</em> of the string was parsed and ignore any remaining characters.</li>
 *   <li>Ensures that the parsed object is of some specific class specified at construction time.</li>
 *   <li>If the string can't be fully parsed or is not of the expected class, logs a warning.</li>
 * </ul>
 *
 * @param <T> The expected type of values to be parsed.
 *
 * @author Martin Desruisseaux (IRD)
 * @since 2.4
 * @module
 */
public class LoggedFormat<T> extends Format {
    /**
     * For cross-version compatibility.
     */
    private static final long serialVersionUID = 4578880360344271325L;

    /**
     * The wrapped format.
     */
    private final Format format;

    /**
     * The expected type for the parsed values.
     */
    private final Class<T> type;

    /**
     * The level to use for the messages to be logged.
     */
    private Level level;

    /**
     * The logger where to log warnings, or {@code null} if none.
     *
     * @see #setLogger
     */
    private String logger;

    /**
     * The class to declare in as the warning emitter, or {@code null} if none.
     *
     * @see #setCaller
     */
    private String className;

    /**
     * The method to declare in as the warning emitter, or {@code null} if none.
     *
     * @see #setCaller
     */
    private String methodName;

    /**
     * Creates a new format wrapping the specified one.
     *
     * @param format The format to use for parsing and formatting.
     * @param type   The expected type of parsed values.
     */
    protected LoggedFormat(final Format format, final Class<T> type) {
        this.format = format;
        this.type   = type;
        this.level  = Level.WARNING;
    }

    /**
     * Creates a new format wrapping the specified one.
     *
     * @param <T>    The expected type of parsed values.
     * @param format The format to use for parsing and formatting.
     * @param type   The expected type of parsed values.
     * @return A new format which will delegate parsing to the given format and log failures.
     */
    public static <T> LoggedFormat<T> getInstance(final Format format, final Class<T> type) {
        return new LoggedFormat<>(format, type);
    }

    /**
     * Sets the logger where to send the warnings eventually emitted by the {@link #parse} method.
     *
     * @param logger The logger where to log warnings, or {@code null} if none.
     */
    public void setLogger(final String logger) {
        this.logger = logger;
    }

    /**
     * Sets the logger level for the warnings eventually emitted by the {@link #parse} method.
     * The default value is {@link Level#WARNING}.
     *
     * @param level The new logging level.
     *
     * @since 2.5
     */
    public void setLevel(final Level level) {
        if (level != null) {
            this.level = level;
        }
    }

    /**
     * Sets the {@linkplain LogRecord#setSourceClassName source class name} and
     * {@linkplain LogRecord#setSourceMethodName source method name} for the warnings
     * eventually emitted by the {@link #parse} method.
     *
     * @param caller The class to declare as the warning emitter, or {@code null} if none.
     * @param method The method to declare as the warning emitter, or {@code null} if none.
     */
    public void setCaller(final Class<?> caller, final String method) {
        this.className  = (caller != null) ? caller.getCanonicalName() : null;
        this.methodName = method;
    }

    /**
     * Parses the specified string. If the string can't be parsed, then this method returns
     * {@code null}. If it can be parsed at least partially and is of the kind specified at
     * construction time, then it is returned. If the string has not been fully parsed, then
     * a {@linkplain LogRecord log record} is prepared and logged.
     *
     * @param  text The text to parse, or {@code null}.
     * @return The parsed object, or {@code null} if {@code text} was null or can't be parsed.
     */
    public T parse(String text) {
        if (text == null || (text=text.trim()).isEmpty()) {
            return null;
        }
        final ParsePosition position = new ParsePosition(0);
        final Object value = parseObject(text, position);
        int index = position.getIndex();
        final int error = position.getErrorIndex();
        if (error >= 0 && error < index) {
            index = error;
        }
        if (index < text.length()) {
            doLogWarning(formatUnparsable(text, 0, index, getWarningLocale(), level));
        } else if (value!=null && !type.isInstance(value)) {
            doLogWarning(Errors.getResources(getWarningLocale()).getLogRecord(level,
                    Errors.Keys.IllegalClass_2, value.getClass(), type));
            return null;
        }
        return type.cast(value);
    }

    /**
     * Parses text from a string to produce an object. This method delegates the work to the
     * {@linkplain Format format} specified at construction time. This method to not perform
     * any logging.
     *
     * @param  text The text to parse.
     * @return An object parsed from the string.
     * @throws ParseException if parsing failed.
     */
    @Override
    public Object parseObject(final String text) throws ParseException {
        return format.parseObject(text);
    }

    /**
     * Parses text from a string to produce an object. This method delegates the work to the
     * {@linkplain Format format} specified at construction time. This method to not perform
     * any logging.
     *
     * @param text The text to parse.
     * @param position Index and error index information.
     * @return An object parsed from the string, or {@code null} in case of error.
     */
    @Override
    public Object parseObject(final String text, final ParsePosition position) {
        return format.parseObject(text, position);
    }

    /**
     * Formats the specified object. This method delegates the work to the
     * {@linkplain Format format} specified at construction time.
     *
     * @param value      The object to format.
     * @param toAppendTo The buffer where the text is to be appended.
     * @param position   Identifies a field in the formatted text.
     * @return           The string buffer passed in with formatted text appended.
     */
    @Override
    public StringBuffer format(final Object value, final StringBuffer toAppendTo,
                               final FieldPosition position)
    {
        return format.format(value, toAppendTo, position);
    }

    /**
     * Formats the specified object. This method delegates the work to the
     * {@linkplain Format format} specified at construction time.
     *
     * @param value The object to format.
     * @return The character iterator describing the formatted value.
     */
    @Override
    public AttributedCharacterIterator formatToCharacterIterator(final Object value) {
        return format.formatToCharacterIterator(value);
    }

    /**
     * Logs a warning. The caller is set before to invoke the user-overridable method.
     */
    private void doLogWarning(final LogRecord warning) {
        if (className != null) {
            warning.setSourceClassName(className);
        }
        if (methodName != null) {
            warning.setSourceMethodName(methodName);
        }
        logWarning(warning);
    }

    /**
     * Logs a warning. This method is invoked automatically by the {@link #parse parse} method
     * when a text can't be fully parsed. The default implementation logs the warning to the
     * logger specified by the last call to the {@link #setLogger setLogger} method. Subclasses
     * may override this method if they want to change the log record before the logging.
     *
     * @param warning The warning to log.
     */
    protected void logWarning(final LogRecord warning) {
        if (logger != null) {
            final Logger logger = Logger.getLogger(this.logger);
            if (warning.getLoggerName() == null) {
                warning.setLoggerName(logger.getName());
            }
            logger.log(warning);
        }
    }

    /**
     * Returns the locale to use for formatting warnings. The default implementation returns
     * the {@linkplain Locale#getDefault() default locale}.
     *
     * @return The locale to use for formatting warnings.
     */
    protected Locale getWarningLocale() {
        return Locale.getDefault(Locale.Category.DISPLAY);
    }

    /**
     * Formats an error message for an unparsable string. This method performs the same work that
     * {@link #formatUnparsable(String, int, int, Locale, Level) formatUnparsable(..., Level)},
     * except that the result is returned as a {@link String} rather than a {@link LogRecord}.
     * This is provided as a convenience method for creating the message to give to an
     * {@linkplain Exception#Exception(String) exception constructor}.
     *
     * @param  text The unparsable string.
     * @param  index The parse position. This is usually {@link ParsePosition#getIndex}.
     * @param  errorIndex The index where the error occurred. This is usually
     *         {@link ParsePosition#getErrorIndex}.
     * @param  locale The locale for the message, or {@code null} for the default one.
     * @return A formatted error message.
     *
     * @since 2.5
     */
    public static String formatUnparsable(final String text, final int index,
            final int errorIndex, final Locale locale)
    {
        return (String) doFormatUnparsable(text, index, errorIndex, locale, null);
    }

    /**
     * Formats a log record for an unparsable string. This method is invoked by the
     * {@link #parse parse} method for formatting the log record to be given to the
     * {@link #logWarning} method. It is made public as a convenience for implementors
     * who wish to manage loggings outside this {@code LoggedFormat} class.
     *
     * @param  text The unparsable string.
     * @param  index The parse position. This is usually {@link ParsePosition#getIndex}.
     * @param  errorIndex The index where the error occurred. This is usually
     *         {@link ParsePosition#getErrorIndex}.
     * @param  locale The locale for the log message, or {@code null} for the default one.
     * @param  level The log record level.
     * @return A formatted log record.
     *
     * @since 2.5
     */
    public static LogRecord formatUnparsable(final String text, final int index,
            final int errorIndex, final Locale locale, Level level)
    {
        if (level == null) {
            // It is necessary to ensure that the level argument is non-null,
            // otherwise we would get a ClassCastException in the code below.
            level = Level.WARNING;
        }
        return (LogRecord) doFormatUnparsable(text, index, errorIndex, locale, level);
    }

    /**
     * Implementation of {@code formatUnparsable} methods. Returns a {@link LogRecord}
     * if {@code level} is non-null, or a {@link String} otherwise.
     */
    private static Object doFormatUnparsable(String text, final int index, int errorIndex,
                                             final Locale locale, final Level level)
    {
        final Errors resources = Errors.getResources(locale);
        final int length = text.length();
        if (errorIndex < index) {
            errorIndex = index;
        }
        if (errorIndex == length) {
            if (level != null) {
                return resources.getLogRecord(level, Errors.Keys.UnexpectedEndOfString);
            }
            return resources.getString(Errors.Keys.UnexpectedEndOfString);
        }
        final String error = CharSequences.token(text, errorIndex).toString();
        text = text.substring(index);
        if (level != null) {
            return resources.getLogRecord(level, Errors.Keys.UnparsableString_2, text, error);
        }
        return resources.getString(Errors.Keys.UnparsableString_2, text, error);
    }

    /**
     * Returns a string representation for debugging purpose.
     */
    @Override
    public String toString() {
        final StringBuilder buffer = new StringBuilder(Classes.getShortClassName(this))
                .append('[').append(Classes.getShortClassName(format));
        if (logger != null) {
            buffer.append(", logger=").append(logger);
        }
        return buffer.append(']').toString();
    }
}
